OBJECTS

Python supports many different kinds of data

  • 123, int
  • 43.14159, float
  • "Hello", string
  • [1, 5, 7, 11, 13], List
  • {"CA": "California", "MA": "Massachusetts"}, Dictonary

Each is an instance of an object, and every object has:

  • a type
  • an internal data representation (primitive or composite)
  • a set of procedures for interaction with the object

Each instanceis a particular type of object

  • 1234 is an instance of an int
  • a = "hello" a is an instance of a string

OBJECT ORIENTED PROGRAMMING (OOP)

Everything in Python is an object and has a type

Objects are a data abstraction that capture:

  • internal representation through data attributes
  • interface for interacting with object through methods (procedures), defines behaviors but hides implementation

Can create new instances of objects

Can destroy objects

  • explicitly using delor just “forget” about them
  • Python system will reclaim destroyed or inaccessible objects –called “garbage collection”

ADVANTAGES OF OOP

Bundle data into packages together with procedures that work on them through well-defined interfaces

Divide-and-conquer development

  • implement and test behavior of each class separately
  • increased modularity reduces complexity

Classes make it easy to reuse code

  • many Python modules define new classes
  • each class has a separate environment (no collision on function names)
  • inheritance allows subclasses to redefine or extend a selected subset of a superclass’ behavior

DEFINE YOUR OWN TYPES

Use the classkeyword to define a new type

class Coordinate(object): <br>

 - define attributes here

Similar to def, indent code to indicate which statements are part of the class definition

The word object means that Coordinateis a Python object and inherits all its attributes

  • Coordinate is a subclass of object
  • object is a superclass of Coordinate

WHAT ARE ATTRIBUTES?

Data and procedures that “belong” to the class

  • data attributes

    • think of data as other objects that make up the class
    • for example, a coordinate is made up of two numbers
  • procedural attributes (methods)

    • think of methods as functions that only work with this class
    • for example you can define a distance between two coordinate objects but there is no meaning to a distance between two list objects
In [1]:
class Coordinate(object):
    def __init__(self, x, y): # a special method called __init__to initialize some data attributes
        self.x = x
        self.y = y
In [2]:
c = Coordinate(3,4)
print(c)
<__main__.Coordinate object at 0x10e5dbbe0>
In [3]:
print(c.x)
3
In [5]:
origin = Coordinate(0,0)
origin.y
Out[5]:
0

Data attributes of an instance are called instance variables

Don’t provide argument for self, Python does this automatically

In [6]:
class Coordinate(object):
    def __init__(self, x, y):
        self.x= x
        self.y= y
    def distance(self, other):
        x_diff_sq= (self.x-other.x)**2
        y_diff_sq= (self.y-other.y)**2
        return(x_diff_sq+y_diff_sq)**0.5
In [7]:
c = Coordinate(3,4)
origin = Coordinate(0,0)
c.distance(origin) # python by default realises c is an instance of a Coordinate
Out[7]:
5.0
In [9]:
Coordinate.distance(c,origin) #another way of calling the same class
Out[9]:
5.0

Python calls the str method when used with printon your class object

In [10]:
print(c) # prints some thing that dosen't make much sense 
<__main__.Coordinate object at 0x10e6805c0>
In [15]:
#adding __str__ method 
class Coordinate(object):
    def __init__(self, x, y):
        self.x= x
        self.y= y
    def distance(self, other):
        x_diff_sq= (self.x-other.x)**2
        y_diff_sq= (self.y-other.y)**2
        return(x_diff_sq+y_diff_sq)**0.5
    def __str__(self): #special method 
        return"<" + str(self.x) + "," + str(self.y) + ">"
In [17]:
c = Coordinate(3,4)
print(c) #prints as defined in __str__ method
<3,4>
In [19]:
print(isinstance(c, Coordinate)) # to check if an object is a Coordinate
True
#### Special predefined methods - __add__(self, other) self + other - __sub__(self, other) self -other - __eq__(self, other) self == other - __lt__(self, other) self < other - __len__(self) len(self) - __str__(self) print(self) ... and others

Example - 1

In [26]:
#Prints numbers in fraction format 

class fraction(object):
    def __init__(self, numer, denom):
        self.numer= numer
        self.denom= denom
    def __str__(self):
        return str(self.numer) + ' / ' + str(self.denom)
In [21]:
fraction(2,3)
Out[21]:
<__main__.fraction at 0x10e68a048>
In [27]:
f = fraction(2,3)
print(f)
2 / 3
In [31]:
# Adding getters , getNumer and getDenom
# To sepearate the ineternal representation from external , simply put not to mess with original value.

class fraction(object):
    def __init__(self, numer, denom):
        self.numer= numer
        self.denom= denom
        
    def __str__(self):
        return str(self.numer) + ' / ' + str(self.denom)
    
    def getNumer(self):
        return self.numer
    
    def getDenom(self):
        return self.denom
In [33]:
f = fraction(2,3)
f.getNumer()
Out[33]:
2
In [34]:
f.getDenom()
Out[34]:
3
In [43]:
#Adding operations to fractions addition and subtraction.

class fraction(object):
    
    def __init__(self, numer, denom):
        self.numer= numer
        self.denom= denom
        
    def __str__(self):
        return str(self.numer) + ' / ' + str(self.denom)
    
    def getNumer(self):  # Getter methods are better practice than just accessing an attribute directly
        return self.numer
    
    def getDenom(self):
        return self.denom
    
    def __add__(self, other): # regular addition is replaced by this method 
        numerNew = other.getDenom() * self.getNumer() \
                    + other.getNumer() * self.getDenom()
        denomNew= other.getDenom() * self.getDenom()
        return fraction(numerNew, denomNew)

    def __sub__(self, other):# regular substration is replaced by this method 
        numerNew = other.getDenom() * self.getNumer() \
                    -other.getNumer() * self.getDenom()
        denomNew = other.getDenom() * self.getDenom()
        return fraction(numerNew, denomNew)
    
    def convert(self): #convert to decimal from fraction 
        return self.getNumer() / self.getDenom()
In [50]:
f = fraction(2,3)
g = fraction(4,5)
In [47]:
f.getNumer()
Out[47]:
2
In [48]:
g.getDenom()
Out[48]:
5
In [46]:
print(f + g)
22 / 15
In [45]:
print(f - g)
-2 / 15
In [49]:
f.convert()
Out[49]:
0.6666666666666666

Example - 2

In [64]:
# a class to creat a list, check if member and remove from the list.

class intSet(object):
    
    def __init__(self):
        self.vals= []
    
    def insert(self, e):
        if not e in self.vals:
            self.vals.append(e)
            
    def member(self, e):
        return e in self.vals
    
    def remove(self, e):
        try:
            self.vals.remove(e)
        except:
            raise ValueError(str(e) + ' not found')
        
    def __str__(self):
        self.vals.sort()
        result = ''
        for e in self.vals:
            result = result + str(e) + ','
        return'{' + result[:-1] + '}'
In [65]:
a = intSet() 
a.member(3)
Out[65]:
False
In [66]:
print(a)
{}
In [67]:
a.insert(3)
In [68]:
print(a)
{3}
In [69]:
a.insert(4)
a.insert(5)
a.insert(3) 
In [70]:
print(a) #note 3 is not repeated 
{3,4,5}
In [71]:
a.member(3)
Out[71]:
True
In [72]:
a.member(5)
Out[72]:
True
In [73]:
a.member(6)
Out[73]:
False
In [74]:
print(a)
{3,4,5}
In [75]:
a.remove(3)
In [76]:
print(a)
{4,5}
In [77]:
a.remove(6) #Throws an error as expected 
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-64-f5eac6e57a25> in remove(self, e)
     16         try:
---> 17             self.vals.remove(e)
     18         except:

ValueError: list.remove(x): x not in list

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-77-b5608789f21a> in <module>()
----> 1 a.remove(6)

<ipython-input-64-f5eac6e57a25> in remove(self, e)
     17             self.vals.remove(e)
     18         except:
---> 19             raise ValueError(str(e) + ' not found')
     20 
     21     def __str__(self):

ValueError: 6 not found

__str__ and __repr__ <p>

The official Python documentation says __repr__ is used to compute the “official” string representation of an object and __str__ is used to compute the “informal” string representation of an object

In [157]:
import datetime
today = datetime.datetime.now()
In [160]:
str(today) # calls __str__ , readable 
Out[160]:
'2018-08-05 06:52:41.933367'
In [162]:
repr(today) #calls __str__ , offical 
Out[162]:
'datetime.datetime(2018, 8, 5, 6, 52, 41, 933367)'
In [163]:
eval('2018-08-05 06:52:41.933367') #str can not be reversed to datetime
Traceback (most recent call last):

  File "/anaconda3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2910, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)

  File "<ipython-input-163-5a248095457a>", line 1, in <module>
    eval('2018-08-05 06:52:41.933367')

  File "<string>", line 1
    2018-08-05 06:52:41.933367
          ^
SyntaxError: invalid token
In [166]:
eval('datetime.datetime(2018, 8, 5, 6, 52, 41, 933367)') #repr can , hence “official” string 
Out[166]:
datetime.datetime(2018, 8, 5, 6, 52, 41, 933367)

Thus in a general every class you code must have a __repr__ and if you think it would be useful to have a string version of the object, as in the case of datetime create a __str__ function.

THE POWER OF OOP

  • Bundle together objects that share

    • common attributes and
    • procedures that operate on those attributes
  • Use abstraction to make a distinction between how to implement an object vs how to use the object

  • Build layers of object abstractions that inherit behaviors from other classes of objects

  • Create our own classes of objects on top of Python’s basic classes

CLASS DEFINITION INSTANCE OF AN OBJECT TYPE vs OF A CLASS

Class is the type

  • a Coordinatetype
  • class Coordinate(object):

Class is defined generically

  • use self to refer to any instance while defining the class

Class defines data and methods common across all instances

Instance is one particular object

  • mycoo= Coordinate(1,2)

Data values vary between instances

  • c1 = Coordinate(1,2)
  • c2 = Coordinate(3,4)
  • C1 and c2 have different data values because they are different objects

instance has the structure of the class

In [168]:
class Animal(object):
    def __init__(self, age):
        self.age= age
        self.name= None
myanimal= Animal(3)
In [169]:
myanimal.age
Out[169]:
3
In [171]:
myanimal.name

getters and setters should be used outside of class to access data attributes

In [173]:
class Animal(object):
    def __init__(self, age):
        self.age= age
        self.name= None
    def get_age(self):
        return self.age
    def get_name(self):
        return self.name
    def set_age(self, newage):
        self.age= newage
    def set_name(self, newname=""): #gives "" as default output
        self.name= newname
    def __str__(self):
        return "animal:"+str(self.name)+":"+str(self.age)
In [174]:
animal1 = Animal(10)
In [176]:
animal1.get_age()
Out[176]:
10
In [177]:
animal1.get_name()
In [180]:
animal1.set_name("rocky")
In [181]:
animal1.get_name()
Out[181]:
'rocky'

INFORMATION HIDING

Author of class definition may change data attribute variable names

If you are accessing data attributes outside the class and class definition changes, may get errors

Outside of class, use getters and setters instead

  • use a.get_age()
  • NOT a.age
  • good style
  • easy to maintain code
  • prevents bugs
In [183]:
#example age is stored as year
class Animal(object):
    def __init__(self, age):
        self.years= age
    def get_age(self):
        return self.years

PYTHON NOT GREAT AT INFORMATION HIDING

Allows you to access data from outside class definition

  • print(a.age)

Allows you to write to data from outside class definition

  • a.age= 'infinite'

Allows you to create data attributes for an instance from outside class definition

  • a.size= "tiny"

It’s not good style to do any of these!

HIERARCHIES

Parent class(superclass)

child class(subclass)

  • inherits all data and behaviors of parent class
  • add more info
  • add more behavior
  • override behavior
In [185]:
#regualr animal class as before
class Animal(object):
    def __init__(self, age):
        self.age= age
        self.name= None
    def get_age(self):
        return self.age
    def get_name(self):
        return self.name
    def set_age(self, newage):
        self.age= newage
    def set_name(self, newname=""): #gives "" as default output
        self.name= newname
    def __str__(self):
        return "animal:"+str(self.name)+":"+str(self.age)
In [187]:
# Cat sub class
class Cat(Animal):
    def speak(self):
        print("meow")
    def __str__(self):
        return "cat:"+str(self.name)+":"+str(self.age)

add new functionality with speak()

  • instance of type Catcan be called with new methods
  • instance of type Animalthrows error if called with new methods

__init__ is not missing, uses the Animal version

In [189]:
jelly = Cat(1)
jelly.set_name('JellyBelly')
print(jelly)
cat:JellyBelly:1
In [192]:
print(Animal.__str__(jelly)) #refrencing the parent class
animal:JellyBelly:1
In [196]:
# Another sub class

class Rabbit(Animal):
    def speak(self):
        print("meep")
    def __str__(self):
        return "rabbit: "+str(self.name)+":"+str(self.age)
In [197]:
jelly = Cat(1)
blob = Animal(1)
peter = Rabbit(5)
jelly.speak()
meow
In [198]:
peter.speak()
meep
In [200]:
blob.speak() #blob dosen't have attribute speak
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-200-0bdce68c74b6> in <module>()
----> 1 blob.speak() #blob dosen't have attribute speak

AttributeError: 'Animal' object has no attribute 'speak'

WHICH METHOD TO USE?

  • subclass can have methods with same name as superclass
  • subclass can have methods with same name as other subclasses
  • for an instance of a class, look for a method name in current class definition
  • if not found, look for method name up the hierarchy (in parent, then grandparent, and so on)
  • use first method up the hierarchy that you found with that method name
In [203]:
#person subclass in Animal class
class Person(Animal):
    def __init__(self, name, age):
        Animal.__init__(self, age)
        Animal.set_name(self, name)
        self.friends= []
    def get_friends(self):
        return self.friends
    def add_friend(self, fname):
        if fname not in self.friends:
            self.friends.append(fname)
    def speak(self):
        print("hello")
    def age_diff(self, other):
        # alternate way: diff = self.age-other.age
        diff = self.get_age() -other.get_age()
        if self.age> other.age:
            print(self.name, "is", diff, "years older than", other.name)
        else:
            print(self.name, "is", -diff, "years younger than", other.name)
    def __str__(self):
        return "person:"+str(self.name)+":"+str(self.age)
In [205]:
eric= Person("Eric", 45)
In [207]:
john = Person("John", 55)
In [208]:
eric.speak()
hello
In [209]:
eric.age_diff(john)
Eric is 10 years younger than John
In [210]:
Person.age_diff(john,eric)
John is 10 years older than Eric
  • sub classes inherit all data attributes and methods of the parent class

  • tag used to give unique id to each new rabbit instance

In [256]:
class Rabbit(Animal):
    tag = 1
    def __init__(self, age, parent1=None, parent2=None):
        Animal.__init__(self, age)
        self.parent1 = parent1
        self.parent2 = parent2
        self.rid= Rabbit.tag
        Rabbit.tag += 1
    def get_rid(self):
        return str(self.rid).zfill(3)
    def get_parent1(self):
        return self.parent1
    def get_parent2(self):
        return self.parent2
    
    def __add__(self, other): #new add method
    # returning object of same type as this class
        return Rabbit(0, self, other)
    
    def __eq__(self, other): # comparing ids of parents since ids are unique
    #decide that two rabbits are equal if they have the same two parents
        parents_same = self.parent1.rid == other.parent1.rid \
        and self.parent2.rid == other.parent2.rid
        parents_opposite = self.parent2.rid == other.parent1.rid \
        and self.parent1.rid == other.parent2.rid
        return parents_same or parents_opposite
In [257]:
peter = Rabbit(2)
peter.set_name('Peter')
hopsy= Rabbit(3)
hopsy.set_name('Hopsy')
cotton = Rabbit(1, peter, hopsy)  # creats new instance with peter and hopsy as parents by calling the classs
cotton.set_name('Cottontail')
print(cotton)
print(cotton.get_parent1())
animal:Cottontail:1
animal:Peter:2
In [258]:
mopsy= peter + hopsy # creats new instance with peter and hopsy as parents  by additon operation
mopsy.set_name('Mopsy')
print(mopsy.get_parent1())
print(mopsy.get_parent2())
animal:Peter:2
animal:Hopsy:3
In [259]:
print(mopsy == cotton)
True

SUMMARY OF CLASSES & OOP

Bundle together objects that share

  • common attributes and
  • procedures that operate on those attributes

Use abstraction to make a distinction between how to implement an object vs how to use the object

Build layers of object abstractions that inherit behaviors from other classes of objects

create our own classes of objects on top of Python’s basic classes


Reference

  • edX course offered by MIT
  • 6.00.1x Introduction to Computer Science and Programming Using Python